#
# peerscape.profile.personal_info
#
# Copyright 2008 Helsinki Institute for Information Technology
# and the authors.
#
# Authors: Ken Rimey <rimey@hiit.fi>
#

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
Edit info fields in personal profile.

Personal profiles include a very flexible set of optional multivalued
text fields, organized in sections.  Each value may be either public
or restricted, where restricted means friends-only (which is the
default).

1. Schema

The standard sections and fields are as follows (with typical English
display labels in parentheses):

 * location
    - country (Country)
    - region (State/Province)
    - city (City)

 * personal
    - gender (Gender = Male/Female)
    - born (Year of Birth)

 * contact
    - website (Website)
    - phone (Phone)
    - email (Email)
    - im (Instant Messaging)

All fields are optional.

All values can be assumed to be a single line of unicode text.

The standard personal and location fields are single-valued.  All
other fields can be assumed to be multivalued.

The gender should be either "Male" or "Female".  A good way to display
it in an input form is as a triplet of radio buttons labeled "Male",
"Female", and "Unspecified".

Application developers are free to add support for additional fields
and sections.  The convention for displaying unknown fields is to
capitalize the field name and use that as the label.

It is also acceptable to allow users to define their own fields by
entering both the label and the value, in which case the label will be
used as the field name.  Such a field name will subsequently be
treated like any other unknown field name.  Note that it might already
be capitalized, might contain spaces, and might in general include
arbitrary unicode characters,

2. Storing Profile Data

The first part of this executable example shows how to store profile
data from a form, overwriting all of the stored data.  In practice,
such a form would have been prefilled with data read from the
database.

>>> from peerscape import DB
>>> db = DB()
>>> iid = db.init_persona('Test User')

>>> info = db.get_blank_personal_info()

>>> info.add_field_value('location', 'country', u'Finland', public=True)
>>> info.add_field_value('location', 'city', u'Helsinki', public=False)
>>> info.add_field_value('location', 'address', u'14 Meadow Lane', public=False)

>>> info.add_field_value('contact', 'website', u'http://example.org/', public=True)
>>> info.add_field_value('contact', 'phone', u'123-4567', public=True)
>>> info.add_field_value('contact', 'phone', u'765-4321', public=False)
>>> info.add_field_value('contact', 'email', u'example@example.com', public=False)
>>> info.add_field_value('contact', 'email', u'example@example.org', public=False)

>>> info.add_field_value('professional', 'company', u'Acme', public=False)

>>> db.set_personal_info(iid, info)

3. Reading Profile Data

Each field value in a PersonalInfo instance has an associated flag
indicating whether it is public or restricted (friends-only).

If we just want to display the profile data, we can disregard these
flags.  We will only get the data that is available to us.

In an input form, each field value will have a check box to indicate
whether it is public.

The PersonalInfo class includes a format() method, which demonstrates
how to display the data or generate an input form.  This method may
also be useful as-is.

>>> info = db.get_personal_info(iid)

>>> from pprint import pprint
>>> pprint(info.format())
[[(u'country', u'Country', u'Finland', True),
  (u'city', u'City', u'Helsinki', False),
  (u'address', u'Address', u'14 Meadow Lane', False)],
 [(u'website', u'Website', u'http://example.org/', True),
  (u'phone', u'Phone', u'123-4567', True),
  (u'phone', u'Phone', u'765-4321', False),
  (u'email', u'Email', u'example@example.com', False),
  (u'email', u'Email', u'example@example.org', False)],
 [(u'company', u'Company', u'Acme', False)]]

>>> pprint(info.format(include_empty_fields=True))
[[(u'country', u'Country', u'Finland', True),
  (u'region', u'State/Province', None, False),
  (u'city', u'City', u'Helsinki', False),
  (u'address', u'Address', u'14 Meadow Lane', False)],
 [(u'gender', u'Gender', None, False),
  (u'born', u'Year of Birth', None, False)],
 [(u'website', u'Website', u'http://example.org/', True),
  (u'phone', u'Phone', u'123-4567', True),
  (u'phone', u'Phone', u'765-4321', False),
  (u'email', u'Email', u'example@example.com', False),
  (u'email', u'Email', u'example@example.org', False),
  (u'im', u'Instant Messaging', None, False)],
 [(u'company', u'Company', u'Acme', False)]]

The intended strategy for displaying the profile fields is to handle
each of the standard sections and then check if there are others, and
for each section, to handle each of the standard fields and then check
if there are others.

The values for a field are retrieved using list_field_values().  The
get_field() method can be used instead if the field is known to be
single-valued:

>>> info.list_field_values('location', 'city')
[(u'Helsinki', False)]
>>> info.get_field('location', 'city')
(u'Helsinki', False)

>>> info.list_field_values('location', 'region')
[]
>>> info.get_field('location', 'region')
(None, False)

After running through the standard fields of the section, we can call
list_field_names() to see if there are any others:

>>> info.list_field_names('location', exclude=['country', 'region', 'city'])
[u'address']

>>> info.list_field_values('location', 'address')
[(u'14 Meadow Lane', False)]

After running through the standard sections, we can call
list_section_names() to see if there are any others:

>>> info.list_section_names(exclude=['location', 'personal', 'contact'])
[u'professional']

>>> info.list_field_names('professional')
[u'company']
>>> info.list_field_values('professional', 'company')
[(u'Acme', False)]

4. Editing Profile Data

Instead of copying all of the data to a form and then creating a new
PersonalInfo instance with the updated data, an application can
alternatively modify the instance in place before writing it back out
to the database.  This is an easy way to edit just a few fields while
preserving unknown fields.

>>> info = db.get_personal_info(iid)
>>> info.set_field('location', 'city', u'Espoo', public=True)
>>> db.set_personal_info(iid, info)

5. Reading a Stranger's Profile Data

Finally, here is a quick test of reading profile data when the
restricted data is not accessible:

>>> db.clear_persona()
>>> info = db.get_personal_info(iid)
>>> pprint(info.format())
[[(u'country', u'Country', u'Finland', True),
  (u'city', u'City', u'Espoo', True)],
 [(u'website', u'Website', u'http://example.org/', True),
  (u'phone', u'Phone', u'123-4567', True)]]

>>> db.close()
"""

from __future__ import with_statement

TEMPLATE = [(u'location', [(u'country', u'Country'),
                          (u'region', u'State/Province'),
                          (u'city', u'City')]),
            (u'personal', [(u'gender', u'Gender'),
                          (u'born', u'Year of Birth')]),
            (u'contact', [(u'website', u'Website'),
                         (u'phone', u'Phone'),
                         (u'email', u'Email'),
                         (u'im', u'Instant Messaging')])]

PUBLIC_PATH = '/public/profile.json'
RESTRICTED_PATH = '/restricted/profile.json'

class PersonalInfoDBMixin(object):
    def get_blank_personal_info(self):
        """Construct an empty PersonalInfo instance."""
        return PersonalInfo()

    def get_personal_info(self, iid):
        """Construct a PersonalInfo instance from a personal profile."""
        with self.read_transaction():
            info = PersonalInfo()
            info.public_info = self.__get_info(iid, PUBLIC_PATH)
            try:
                cipher=self.get_group_cipher(iid)
                info.restricted_info = self.__get_info(iid, RESTRICTED_PATH,
                                                       cipher=cipher)
            except LookupError:
                pass

            return info

    def set_personal_info(self, iid, info, event_message='Updated personal info.'):
        """Write the data from a PersonalInfo instance to a personal profile."""
        with self.write_transaction():
            mark1 = self.get_current_mark()

            self.__set_info(iid, PUBLIC_PATH, info.public_info)

            mark2 = self.get_current_mark()

            self.__set_info(iid, RESTRICTED_PATH, info.restricted_info,
                            cipher=self.get_group_cipher(iid))

            mark3 = self.get_current_mark()

            if mark1 != mark3:
                self.post_event(iid, event_message,
                                public=(mark1 != mark2))

    def __get_info(self, iid, path, cipher=None):
        data = self.get_json(iid, path, cipher=cipher)
        return Info(data) if data else Info()

    def __set_info(self, iid, path, info, cipher=None):
        if info:
            self.put(iid, path, info, cipher=cipher)
        else:
            self.delete(iid, path)

class PersonalInfo(object):
    def __init__(self):
        self.public_info = Info()
        self.restricted_info = Info()

    def get_field(self, section_name, field_name):
        for value, flag in self.list_field_values(section_name, field_name):
            return value, flag
        else:
            return None, False

    def set_field(self, section_name, field_name, value, public=False):
        self.clear_field_values(section_name, field_name)
        self.add_field_value(section_name, field_name, value, public)

    def list_field_values(self, section_name, field_name):
        public_values = \
            self.public_info.list_field_values(section_name, field_name)
        restricted_values = \
            self.restricted_info.list_field_values(section_name, field_name)
        return ([(v, True) for v in public_values]
                + [(v, False) for v in restricted_values])

    def add_field_value(self, section_name, field_name, value, public=False):
        if public:
            self.public_info.add_field_value(section_name, field_name, value)
        else:
            self.restricted_info.add_field_value(section_name, field_name, value)

    def clear_field_values(self, section_name, field_name):
        self.public_info.clear_field_values(section_name, field_name)
        self.restricted_info.clear_field_values(section_name, field_name)

    def list_section_names(self, exclude=[]):
        result = set(self.public_info.list_section_names(exclude))
        result.update(self.restricted_info.list_section_names(exclude))
        return sorted(result)

    def list_field_names(self, section_name, exclude=[]):
        result = set(self.public_info.list_field_names(section_name, exclude))
        result.update(self.restricted_info.list_field_names(section_name, exclude))
        return sorted(result)

    def format(self, template=TEMPLATE, include_empty_fields=False):
        known_sections = [item[0] for item in template]

        output = []
        for section, section_template in template:
            output_chunk = self.format_section(section, section_template,
                                               include_empty_fields)
            if output_chunk:
                output.append(output_chunk)

        for section in self.list_section_names(exclude=known_sections):
            output.append(self.format_section(section))

        return output

    def format_section(self, section, template=[], include_empty_fields=False):
        known_fields = [item[0] for item in template]

        output = []
        for field, label in template:
            data = self.list_field_values(section, field)
            for value, flag in data:
                output.append((field, label, value, flag))

            if not data and include_empty_fields:
                output.append((field, label, None, False))

        for field in self.list_field_names(section, exclude=known_fields):
            label = field.capitalize()
            for value, flag in self.list_field_values(section, field):
                output.append((field, label, value, flag))

        return output

class Info(dict):
    """
    Two-level multivalued dictionary

    >>> i = Info()

    >>> i.list_field_values('location', 'city')
    []
    >>> i.add_field_value('location', 'city', 'Helsinki')
    >>> i.list_field_values('location', 'city')
    ['Helsinki']

    >>> i.list_field_values('contact', 'phone')
    []
    >>> i.add_field_value('contact', 'phone', '(555) 123-4567')
    >>> i.list_field_values('contact', 'phone')
    ['(555) 123-4567']
    >>> i.add_field_value('contact', 'phone', '(555) 765-4321')
    >>> i.list_field_values('contact', 'phone')
    ['(555) 123-4567', '(555) 765-4321']
    >>> i.add_field_value('contact', 'phone', '(555) 000-0000')
    >>> i.list_field_values('contact', 'phone')
    ['(555) 123-4567', '(555) 765-4321', '(555) 000-0000']

    >>> i.add_field_value('contact', 'email', 'somebody@example.org')
    >>> i.list_field_values('contact', 'email')
    ['somebody@example.org']

    >>> i.list_section_names()
    ['contact', 'location']
    >>> i.list_section_names(exclude=['location', 'personal'])
    ['contact']

    >>> i.list_field_names('hunos')
    []
    >>> i.list_field_names('contact')
    ['email', 'phone']
    >>> i.list_field_names('contact', exclude=['email', 'website'])
    ['phone']
    """

    def list_field_values(self, section_name, field_name):
        section = self.get(section_name, {})
        value = section.get(field_name)
        if isinstance(value, list):
            return value
        elif value:
            return [value]
        else:
            return []

    def add_field_value(self, section_name, field_name, value):
        if value:
            section = self.setdefault(section_name, {})
            current = section.get(field_name)
            if isinstance(current, list):
                current.append(value)
            elif current:
                section[field_name] = [current, value]
            else:
                section[field_name] = value

    def clear_field_values(self, section_name, field_name):
        if section_name in self:
            section = self[section_name]
            if field_name in section:
                del section[field_name]
            if not section:
                del self[section_name]

    def list_section_names(self, exclude=[]):
        return sorted(set(self.keys()).difference(exclude))

    def list_field_names(self, section_name, exclude=[]):
        section = self.get(section_name, {})
        return sorted(set(section.keys()).difference(exclude))

def _test_event_logging(self):
    """
    >>> from peerscape import DB
    >>> db = DB()
    >>> iid = db.init_persona('Test User')
    >>> db.set_profile_name(iid, 'Joe')
    >>> db.get_last_event(iid)['comment']
    u'Changed name from "Test User".'

    >>> info = db.get_personal_info(iid)
    >>> info.set_field('location', 'city', u'Helsinki', public=False)
    >>> db.set_personal_info(iid, info)
    >>> db.get_last_event(iid)['comment']
    u'Updated personal info.'

    >>> db.post_event(iid, 'Testing...', public=False)
    >>> db.set_personal_info(iid, info)
    >>> db.get_last_event(iid)['comment']
    u'Testing...'

    >>> db.clear_persona()
    >>> db.get_last_event(iid)['comment']
    u'Changed name from "Test User".'

    >>> db.close()
    """
    pass

def _test_event_logging2(self):
    """
    >>> from peerscape import DB
    >>> db = DB()
    >>> iid = db.init_persona('Test User')
    >>> db.set_profile_name(iid, 'Joe')
    >>> db.get_last_event(iid)['comment']
    u'Changed name from "Test User".'

    This time we make the city public:
    >>> info = db.get_personal_info(iid)
    >>> info.set_field('location', 'city', u'Helsinki', public=True)
    >>> db.set_personal_info(iid, info)
    >>> db.get_last_event(iid)['comment']
    u'Updated personal info.'

    >>> db.post_event(iid, 'Testing...', public=True)
    >>> db.set_personal_info(iid, info)
    >>> db.get_last_event(iid)['comment']
    u'Testing...'

    >>> db.close()
    """
    pass

if __name__ == '__main__':
    import doctest
    doctest.testmod()
